%==========================================================================
%=  This file is part of the Sound Restoration Project
%=  (c) Copyright Industrial Mathematics Institute
%=                University of South Carolina, Department of Mathematics
%=  ALL RIGHTS RESERVED
%=
%=  Author: Borislav Karaivanov
%==========================================================================

%==========================================================================
% List of the files on which this procedure depends:
%
% interpretInputArguments.m
% getAllSoundSignals.m
% getSoundTrackBounds.m
% estimateSoundTrackBounds.m
% getSoundTrackBoundsManually.m
% readImageFile.m
% readdpx.m
% writeToLogFile.m
% Image processing toolbox
% readImageByIndex.m
% extractSoundSignals.m
% write2DArrayToFile.m
% read2DArrayFromFile.m
% combine2DArraysFromFiles.m
% getSizeOf2DArrayFromFile.m
% extractSoundWithSoundOverlapsTest.m
% calibrateSignalsSingleMask.m
% findBestFitToSignal.m
% fitReduceFitSignal.m
% Curve Fitting toolbox
% visualizeSoundCalibrationSingleMask.m
% createMissingDirs.m
% computeOverlapGuess.m
% visualizeOverlapGuess.m
% computeSignalOverlaps.m
% findMinRelativeDifference.m
% checkSignalOverlapsAgainstImages.m
% combineSignals.m
% visualizeOverlaps.m
% saveSound.m
% imagesAndAudioToVideo.m
% imagesToVideo.m
% getFileSequenceFromRepresentative.m
% ffmpeg command line utilities
% tidyLogFile.m
%
%==========================================================================

%==========================================================================
% The function "extractAudio" reads the sound track from a series of film
% scans and generates a number of audio signal.
% INPUT: "projectStruct" is a project structure holding all parameters and
% other information relevant to the sound extraction from a specific frame
% sequence.
% "soundBoundsArr" is a 2D array with two columns specifying the left and
% right vertical bounds of the sound track band in number of pixels. Each
% row corresponds to a pair of left and right bounds. It is expected that 
% 1 <= soundBoundsArr(k, 1) <= soundBoundsArr(k, 2) <= imageWidth for each
% valid value of k.
% OUTPUT: "projectStruct" returns the updated project structure.
%==========================================================================
function projectStruct = extractAudio(projectStruct)

writeToLogFunctHandle = projectStruct.writeToLogFunctHandle;

% Write info to a log file.
writeToLogFunctHandle(['number of frames: ' num2str(projectStruct.lastFrameIndex ...
    - projectStruct.firstFrameIndex + 1)]);
for k = 1:size(projectStruct.soundBoundsArr, 1)
    writeToLogFunctHandle(['left soundtrack bound' num2str(k) ': ' num2str(projectStruct.soundBoundsArr(k, 1))]);
    writeToLogFunctHandle(['right soundtrack bound' num2str(k) ': ' num2str(projectStruct.soundBoundsArr(k, 2))]);
    writeToLogFunctHandle(['soundtrack width' num2str(k) ': ' num2str(projectStruct.soundBoundsArr(k, 2) - projectStruct.soundBoundsArr(k, 1) + 1)]);
end

% Check if the output directoriy exists.
if (exist(projectStruct.outFile.outputDir, 'dir') ~= 7)
    warndlg('Please, specify a valid output directory.');
    return;
end
% Check if the scratch directories exist.
if (exist(projectStruct.outFile.scratchDir1, 'dir') ~= 7)
    warndlg('Please, specify a valid first scratch directory.');
    return;
end
if (exist(projectStruct.outFile.scratchDir2, 'dir') ~= 7)
    warndlg('Please, specify a valid second scratch directory.');
    return;
end

debug ='XA'

try
% Specify the algorithmic steps to be executed.
argInCellArr = {...
    'DoExtractSoundSignals', true, ...
    'DoExtractSoundWithSoundOverlaps', true, ...
    'DoImproveSoundWithImageOverlaps', false, ...
    'DoSaveSound', true, ...
    'DoSyncAudioAndVideo', false};
[~, doStruct] = interpretInputArguments(argInCellArr);

catch exception
    getReport(exception, 'extended', 'hyperlinks', 'on')
    return;
end
debug ='XB'

if (projectStruct.isPolynomialOn == true)
    degOfPolyFitArr = find([projectStruct.isDegree1On, projectStruct.isDegree2On, ...
        projectStruct.isDegree3On, projectStruct.isDegree4On]);
else
    degOfPolyFitArr = [];
end

debug ='XBA'

if (strcmpi(projectStruct.rowsToIgnoreUnits.choicesCellArr{projectStruct.rowsToIgnoreUnits.currChoiceInd}, 'pixels') == true)
    rowsToIgnore = projectStruct.rowsToIgnore;
else
    rowsToIgnore = (projectStruct.rowsToIgnore/100)*projectStruct.height;
end

debug ='XBB'

debug_bitdepth = projectStruct.bitDepth
debug_choices = projectStruct.bitDepth.choicesCellArr

% Set the bit depth of the produced audio file to the maximal allowed.
%bitDepth = max(cellfun(@str2num,  projectStruct.bitDepth.choicesCellArr));

debug ='XC'

% Set up a progress bar with a "Cancel" button.
waitbarHandle = waitbar(0, ...
    sprintf('Extracting sound from %d files', projectStruct.lastFrameIndex - projectStruct.firstFrameIndex + 1), ...
    'CreateCancelBtn', 'setappdata(gcbf, ''canceling'', 1)' );
setappdata(waitbarHandle, 'canceling', 0)

% Specify the cleanup tasks to be performed at function completion.
cleanupObj = onCleanup(@()cleanup_extractAudio(waitbarHandle));

debug ='XD'

partitionLenArr = [0.3, 0.025, 0.3, 0.025, 0.3, 0.05];
temp = projectStruct.lastFrameIndex - projectStruct.firstFrameIndex + 1;
totalsArr = temp(ones(size(partitionLenArr)));
% Reduce the totals pairs.
totalsArr(2) = totalsArr(2) - 1;
totalsArr(3) = 0;
totalsArr(4) = totalsArr(4) - 2;
% Reduce the total for video files.
totalsArr(end) = 2;
waitbarStructArr = setWaitbar(waitbarHandle, partitionLenArr, totalsArr);
currWaitbarInd = 1;

debug ='XE'

%==========================================================================
if (doStruct.doExtractSoundSignals == true)
    tic
    
    % Extract the sound signals from all images and write those signals to
    % a text file.
    scratchFileNamePrefix = [projectStruct.outFile.scratchDir1 projectStruct.inFile.namePrefixNoPath];
    [soundTextFileNameCellArr, allSignalsCellArr] = getAllSoundSignals( ...
        projectStruct.readFramesFunctHandle, ...
        [projectStruct.firstFrameIndex, projectStruct.lastFrameIndex], ...
        projectStruct.soundBoundsArr, ...
        projectStruct.framesPerBatch, ...
        scratchFileNamePrefix, ...
        waitbarStructArr(currWaitbarInd), ...
        writeToLogFunctHandle);

    if getappdata(waitbarHandle, 'canceling')
        return;
    end

    currWaitbarInd = currWaitbarInd + 1;
    timeElapsed = toc;
    writeToLogFunctHandle(['Timing: sound signals extracted in ' num2str(timeElapsed, '%.2f') ' seconds']);
    fprintf(1, 'Timing: sound signals extracted in %f seconds.\n', timeElapsed);
end
%==========================================================================

debug ='XF'


%==========================================================================
if (doStruct.doExtractSoundWithSoundOverlaps == true)
    tic
    
    % Allocate a cell array to store the extracted sound signals.
    signalCellArr = cell(size(soundTextFileNameCellArr));
    calibSignalsCellArr = cell(size(soundTextFileNameCellArr));
    calibSoundTextFileNameCellArr = cell(size(soundTextFileNameCellArr));
    
    % Define a part of the name prefix for an output text file to store the
    % calibrated ound signals.
    scratchFileNamePrefix = [projectStruct.outFile.scratchDir1 projectStruct.inFile.namePrefixNoPath];
    
    for pairInd = 1:size(projectStruct.soundBoundsArr, 1)
%         % Read in the signals from uncalibrated images from a text file.
%         allSignalsArr = read2DArrayFromFile(soundTextFileNameCellArr{pairInd}, writeToLogFunctHandle);
        % Define a name for an output text file to store the calibrated
        % sound signals.
        calibSoundTextFileNameCellArr{pairInd} = [scratchFileNamePrefix ...
            'SndBounds' num2str(projectStruct.soundBoundsArr(pairInd, 1)) ...
            '-' num2str(projectStruct.soundBoundsArr(pairInd, 2)) ...
            '_Fr' num2str(projectStruct.firstFrameIndex) '-' ...
            num2str(projectStruct.lastFrameIndex) '.txt'];
        
        % Extract a single, combined sound signal corresponding to the
        % current pair of sound track boundaries.
        [signalCellArr{pairInd}, calibSignalsCellArr{pairInd}] = ...
            extractSoundWithSoundOverlaps(allSignalsCellArr(:, pairInd), ...
            projectStruct.signalsPerMask, ...
            projectStruct.sweeps, ...
            projectStruct.span, ...
            degOfPolyFitArr, ...
            rowsToIgnore, ...
            projectStruct.framesForOverlapGuess, ...
            projectStruct.initialSearchRadius, ...
            projectStruct.localSearchRadius, ...
            projectStruct.smoothingSpan, ...
            calibSoundTextFileNameCellArr{pairInd}, ...
            waitbarStructArr(currWaitbarInd:(currWaitbarInd + 1)), ...
            writeToLogFunctHandle);
    end
    if getappdata(waitbarHandle, 'canceling')
        return;
    end

    currWaitbarInd = currWaitbarInd + 2;
    
    timeElapsed = toc;
    writeToLogFunctHandle(['Timing: combined sound signals with signal based overlaps produced in ' num2str(timeElapsed, '%.2f') ' seconds']);
    fprintf(1, 'Timing: combined sound signals with signal based overlaps produced in %f seconds.\n', timeElapsed);
end
%==========================================================================

debug ='XG'


%==========================================================================
if (doStruct.doSaveSound == true)
    tic
    
    % Allocate a cell array to store the names of the audio .wav files.
    projectStruct.audioFileNameCellArr = cell(size(calibSoundTextFileNameCellArr));

    debug ='XGA'
    
    for ind = 1:size(projectStruct.soundBoundsArr, 1)
        debug ='XGB'
        
        % Get the name of the text sound file with directory path and file
        % extension removed.
        [~, calibSoundTextFileNameNoPathNoExt] = fileparts(calibSoundTextFileNameCellArr{ind});
            debug ='XGC'

        % Construct a file name prefix for the audio file.
        soundFileNamePrefix = [projectStruct.outFile.outputDir calibSoundTextFileNameNoPathNoExt '_SndOverlaps'];
            debug ='XGD'

        % Save the sound signal as an audio .wav file.
        projectStruct.samplingRate.native = floor(numel(signalCellArr{ind})/(size(calibSignalsCellArr{ind}, 2)/projectStruct.frameRate));
            debug ='XGE'

        debug_samplingRate = getSamplingRate(projectStruct)
            debug ='XGF'


        projectStruct.audioFileNameCellArr{ind} = ...
            saveSound(signalCellArr{ind}, ...
            soundFileNamePrefix, ...
            projectStruct.samplingRate.native, ...
            projectStruct.maxAmplitude, ...
            projectStruct.bitDepth.choicesCellArr{projectStruct.bitDepth.currChoiceInd}, ...
            getSamplingRate(projectStruct), ...
            writeToLogFunctHandle);
        debug ='XGG'

    end
    
    if getappdata(waitbarHandle, 'canceling')
        return;
    end

    timeElapsed = toc;
    writeToLogFunctHandle(['Timing: single sound signal saved in ' num2str(timeElapsed, '%.2f') ' seconds']);
    fprintf(1, 'Timing: single sound signal saved in %f seconds.\n', timeElapsed);
end
%==========================================================================

debug ='XH'


% %==========================================================================
% if (doStruct.doImproveSoundWithImageOverlaps == true)
%     tic
%     
%     % If the cell array storing the calibrated pieces of sound signal does
%     % not exist or it is empty, then read in those calibrated sound signals
%     % from the corresponding text files.
%     if ( (exist('calibSignalsCellArr', 'var') == false)||(isempty(calibSignalsCellArr) == true) )
%         % Allocate a cell column vector to store the calibrated pieces of
%         % sound signal.
%         calibSignalsCellArr = cell(size(soundTextFileNameCellArr));
%         % Read in the calibrated pieces of sound signal.
%         for pairInd = 1:numel(soundTextFileNameCellArr)
%             calibSignalsCellArr{pairInd} = read2DArrayFromFile(calibSoundTextFileNameCellArr{pairInd}, writeToLogFunctHandle);
%         end
%     end
%     % Try to improve the quality of the extracted sound using the whole
%     % images to determine more precise overlaps when deemed necessary to
%     % double-check.
%     signalArr = improveSoundWithImageOverlaps(calibSignalsCellArr, ...
%         imageFileNamePrefix, fileIndArr, imageBasedCalibMaskArr, ...
%         projectStruct.signalsPerMask, projectStruct.sweeps, projectStruct.span, degOfPolyFitArr, ...
%         projectStruct.framesForOverlapGuess, projectStruct.initialSearchRadius, ...
%         projectStruct.localSearchRadius, projectStruct.workFile.indFieldLength, ...
%         imageFileExt, waitbarStructArr(currWaitbarInd:(currWaitbarInd + 2)));
%     
%     if getappdata(waitbarHandle, 'canceling')
%         return;
%     end
% 
%     currWaitbarInd = currWaitbarInd + 3;
%     
% %     % Read in the signals from calibrated images from a text file.
% %     imageCalibAllSignalsArr = read2DArrayFromFile(imageCalibSoundTextFileName, writeToLogFunctHandle);
% %     globalShotFileNamePrefix = [globalShotFileNamePrefix 'ImCalib_'];
% %     imageCalibSignalArr = extractSoundWithSoundOverlapsTest(imageCalibAllSignalsArr, ...
% %         imageFileNamePrefix, fileIndArr, imageBasedCalibMaskArr, ...
% %         projectStruct.signalsPerMask, projectStruct.sweeps, projectStruct.span, degOfPolyFitArr, ...
% %         projectStruct.framesForOverlapGuess, projectStruct.initialSearchRadius, projectStruct.localSearchRadius, projectStruct.workFile.indFieldLength, imageFileExt);
% %     globalShotFileNamePrefix = globalShotFileNamePrefix(1:(end - 8));
% 
%     
% %     allSignalsCalibArr = read2DArrayFromFile(imageCalibSoundTextFileName, writeToLogFunctHandle);
% %     globalShotFileNamePrefix = [globalShotFileNamePrefix 'ImCalib_'];
% %     signalCalibArr = extractSoundWithSoundOverlaps(allSignalsCalibArr, projectStruct.signalsPerMask, ...
% %         projectStruct.sweeps, projectStruct.span);
% %     globalShotFileNamePrefix = globalShotFileNamePrefix(1:(end - 8));
%     
%     timeElapsed = toc;
%     writeToLogFunctHandle(['Timing: combined sound signals corrected with image based overlaps in ' num2str(timeElapsed, '%.2f') ' seconds']);
%     fprintf(1, 'Timing: combined sound signals corrected with image based overlaps in %f seconds.\n', timeElapsed);
% end
% %==========================================================================
% 
% 
% %==========================================================================
% if (doStruct.doSaveSound == true)
%     tic
%     
%     % Get the name of the text sound file with directory path and file
%     % extension removed.
%     [~, soundTextFileNameNoPathNoExt] = fileparts(soundTextFileName);
%     % Construct a file name prefix for the audio file.
%     soundFileNamePrefix = [outDirPath soundTextFileNameNoPathNoExt '_SndOverlaps'];
%     % Save the sound signal as an audio .wav file.
%     samplingRate = floor(numel(signalArr)/(size(allSignalsArr, 2)/projectStruct.frameRate));
%     saveSound(signalArr, soundFileNamePrefix, samplingRate, projectStruct.maxAmplitude, bitDepth, samplingRate);
%     saveSound(signalArr, soundFileNamePrefix, samplingRate, projectStruct.maxAmplitude, 24, 48000);
%     saveSound(signalArr, soundFileNamePrefix, samplingRate, projectStruct.maxAmplitude, 24, 96000);
%     audioFileName = saveSound(signalArr, soundFileNamePrefix, samplingRate, projectStruct.maxAmplitude, 16, 48000);
% 
%     if getappdata(waitbarHandle, 'canceling')
%         return;
%     end
% 
%     timeElapsed = toc;
%     writeToLogFunctHandle(['Timing: single sound signal saved in ' num2str(timeElapsed, '%.2f') ' seconds']);
%     fprintf(1, 'Timing: single sound signal saved in %f seconds.\n', timeElapsed);
% end
% %==========================================================================
% 
% 
% 
% %==========================================================================
% if (doStruct.doSyncAudioAndVideo == true)
%     tic
%     
%     % Save the sound signal as an audio .wav file.
%     imageFileName = [inDirPath imageFileNamePrefixNoPath num2str(fileIndArr(1), ...
%         ['%0' num2str(projectStruct.workFile.indFieldLength) 'd']) '.' imageFileExt];
%     if (ispc)
%         videoFileSuffix = '.mp4';
%         movieFileSuffix = '.mov';
%     else
%         videoFileSuffix = '.avi';
%         movieFileSuffix = '.mov';
%     end
%     desiredSizesCellArr = {300; 1080};
%     videoQuality = 75;
%     movieFileNameCellArr = imagesAndAudioToMovie(audioFileName, imageFileName, ...
%         fileIndArr(1), fileIndArr(end), outDirPath, movieFileSuffix, scratchDirPath, ...
%         videoFileSuffix, audioDelay, projectStruct.frameRate, desiredSizesCellArr, ...
%         videoQuality, true, waitbarStructArr(currWaitbarInd:(currWaitbarInd + 1)));
%     
%     if getappdata(waitbarHandle,'canceling')
%         return
%     end
% 
%     timeElapsed = toc;
%     writeToLogFunctHandle(['Timing: audio and video synchronized in ' num2str(timeElapsed, '%.2f') ' seconds']);
%     fprintf(1, 'Timing: audio and video synchronized in %f seconds.\n', timeElapsed);
% end
% %==========================================================================


% Write info to a log file.
writeToLogFunctHandle(['index field length: ' num2str(projectStruct.workFile.indFieldLength)]);
writeToLogFunctHandle(['number of frames per second: ' num2str(projectStruct.frameRate)]);

return;
end  % end of the function "extractAudio"



%==========================================================================
% The cleanup routine called whenever the function "extractAudio"
% 'return's.
%==========================================================================
function cleanup_extractAudio(waitbarHandle)
% Get rid of the progress bar.
delete(waitbarHandle);

% Remove duplicate lines and group the remaining lines in the log file.
logFileName = writeToLogFunctHandle('End extract audio');
tidyLogFile(logFileName);

% Get rid of the global variables so that they are not available for any
% subsequently executed code.
clear global globalDoVisualize
clear global globalShotFileNamePrefix
clear global globalShotFileExt

end  % end of the function "cleanup_extractAudio"
